# -*- coding: binary -*-
module Msf

require 'msf/core/exploit/tcp'
require 'rex/mime'

###
#
# This module exposes methods that may be useful to exploits that send email
# messages via SMTP.
#
###

module Exploit::Remote::SMTPDeliver

  include Exploit::Remote::Tcp

  #
  # Creates an instance of an exploit that delivers messages via SMTP
  #
  def initialize(info = {})
    super

    # Register our options, overriding the RHOST/RPORT from TCP
    register_options(
      [
        OptAddress.new("RHOST", [ true, "The SMTP server to send through" ]),
        OptPort.new("RPORT", [ true, "The SMTP server port (e.g. 25, 465, 587, 2525)", 25 ]),
        OptString.new('DATE', [false, 'Override the DATE: field with this value', '']),
        OptString.new('MAILFROM', [ true, 'The FROM address of the e-mail', 'random@example.com' ]),
        OptString.new('MAILTO', [ true, 'The TO address of the email' ]),
        OptString.new('SUBJECT', [ true, 'Subject line of the email' ]),
        OptString.new('USERNAME', [ false, 'SMTP Username for sending email', '' ]),
        OptString.new('PASSWORD', [ false, 'SMTP Password for sending email', '' ]),
        OptString.new('DOMAIN', [false, 'SMTP Domain to EHLO to', '']),
        OptString.new('VERBOSE', [ false, 'Display verbose information' ]),
      ], Msf::Exploit::Remote::SMTPDeliver)
    register_autofilter_ports([ 25, 465, 587, 2525, 25025, 25000])
    register_autofilter_services(%W{ smtp smtps })

    @connected = false
  end

  def connected?
    (@connected)
  end

  #
  # Establish an SMTP connection to host and port specified by the RHOST and
  # RPORT options, respectively.  After connecting, the banner message is
  # read in and stored in the +banner+ attribute.
  #
  # This method does NOT perform an EHLO, it only connects.
  #
  def connect(global = true)
    fd = super

    if fd
      @connected = true
      # Wait for a banner to arrive...
      self.banner = fd.get_once(-1, 30)
    end
    fd
  end

  #
  # Connect to the remote SMTP server, send EHLO, start TLS if the server
  # asks for it, and authenticate if we've got creds (specified in +USERNAME+
  # and +PASSWORD+ datastore options).
  #
  # This method currently only knows about PLAIN authentication.
  #
  def connect_login(global = true)
    print_verbose("Connecting to SMTP server #{rhost}:#{rport}...")
    nsock = connect(global)

    if datastore['DOMAIN'] and not datastore['DOMAIN'] == ''
      domain = datastore['DOMAIN']
    else
      domain = Rex::Text.rand_text_alpha(rand(32)+1)
    end

    res = raw_send_recv("EHLO #{domain}\r\n", nsock)
    if res =~ /STARTTLS/
      print_status("Starting tls")
      raw_send_recv("STARTTLS\r\n", nsock)
      swap_sock_plain_to_ssl(nsock)
      res = raw_send_recv("EHLO #{domain}\r\n", nsock)
    end

    unless datastore['PASSWORD'].empty? and datastore["USERNAME"].empty?
      # TODO: other auth methods
      if res =~ /AUTH .*PLAIN/
        if datastore["USERNAME"] and not datastore["USERNAME"].empty?
          # Have to double the username.  SMTP auth is weird
          user = "#{datastore["USERNAME"]}\0" * 2
          auth = Rex::Text.encode_base64("#{user}#{datastore["PASSWORD"]}")
          raw_send_recv("AUTH PLAIN #{auth}\r\n", nsock)
        else
          print_status("Server requested auth and no creds given, trying to continue anyway")
        end
      elsif res =~ /AUTH .*LOGIN/
        if datastore["USERNAME"] and not datastore["USERNAME"].empty?
          user = Rex::Text.encode_base64("#{datastore["USERNAME"]}")
          auth = Rex::Text.encode_base64("#{datastore["PASSWORD"]}")
          raw_send_recv("AUTH LOGIN\r\n",nsock)
          raw_send_recv("#{user}\r\n",nsock)
          raw_send_recv("#{auth}\r\n",nsock)
        else
          print_status("Server requested auth and no creds given, trying to continue anyway")
        end
      elsif res =~ /AUTH/
        print_error("Server doesn't accept any supported authentication, trying to continue anyway")
      else
        if datastore['PASSWORD'] and datastore["USERNAME"] and not datastore["USERNAME"].empty?
          # Let the user know their creds are going unused
          print_verbose("Server didn't ask for authentication, skipping")
        end
      end
    end

    return nsock
  end


  #
  # Sends an email message, connecting to the server first if a connection is
  # not already established.
  #
  def send_message(data)
    send_status = nil

    already_connected = connected?
    if already_connected
      print_status("Already connected, reusing")
      nsock = self.sock
    else
      nsock = connect_login(false)
    end

    raw_send_recv("MAIL FROM: <#{datastore['MAILFROM']}>\r\n", nsock)
    raw_send_recv("RCPT TO: <#{datastore['MAILTO']}>\r\n", nsock)

    resp = raw_send_recv("DATA\r\n", nsock)

    # If the user supplied a Date field, use that, else use the current
    # DateTime in the proper RFC2822 format.
    if datastore['DATE'].present?
      raw_send_recv("Date: #{datastore['DATE']}\r\n", nsock)
    else
      raw_send_recv("Date: #{DateTime.now.rfc2822}\r\n", nsock)
    end

    # If the user supplied a Subject field, use that
    if datastore['SUBJECT'].present?
      raw_send_recv("Subject: #{datastore['SUBJECT']}\r\n", nsock)
    end

    # Avoid sending tons of data and killing the connection if the server
    # didn't like us.
    if not resp or not resp[0,3] == '354'
      print_error("Server refused our mail")
    else
      send_status = raw_send_recv("#{data}\r\n.\r\n", nsock)
    end

    if not already_connected
      print_verbose("Closing the connection...")
      disconnect(nsock)
    end

    send_status
  end

  def disconnect(nsock=self.sock)
    raw_send_recv("QUIT\r\n", nsock)
    super
    @connected = false
  end

  def raw_send_recv(cmd, nsock=self.sock)
    return false if not nsock
    if cmd =~ /AUTH PLAIN/
      # Don't print the user's plaintext password
      print_verbose("C: AUTH PLAIN ...")
    else
      # Truncate because this will include a full email and we don't want
      # to dump it all.
      print_verbose("C: #{((cmd.length > 120) ? cmd[0,120] + "..." : cmd).strip}")
    end

    nsock.put(cmd)
    res = nsock.get_once

    # Don't truncate the server output because it might be helpful for
    # debugging.
    print_verbose("S: #{res.strip}") if res

    return res
  end

  def print_verbose(msg)
    if datastore['VERBOSE']
      print_status(msg)
    end
  end


  # The banner received after the initial connection to the server.  This should look something like:
  #   220 mx.google.com ESMTP s5sm3837150wak.12
  attr_reader :banner

protected
  attr_writer :banner #:nodoc:

  #
  # Create a new SSL session on the existing socket.  Used for STARTTLS
  # support.
  #
  def swap_sock_plain_to_ssl(nsock=self.sock)
    ctx = generate_ssl_context()
    ssl = OpenSSL::SSL::SSLSocket.new(nsock, ctx)

    ssl.connect

    nsock.extend(Rex::Socket::SslTcp)
    nsock.sslsock = ssl
    nsock.sslctx  = ctx
  end

  def generate_ssl_context
    ctx = OpenSSL::SSL::SSLContext.new
    ctx.key = OpenSSL::PKey::RSA.new(1024){ }

    ctx.session_id_context = Rex::Text.rand_text(16)

    return ctx
  end

end

end
